#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Judith Robson - CellMates

import sys
import Globals
import Widgets
import Options
import autoCellmodif
from PySide.QtCore import *
from PySide.QtGui import *

""" Classe "MAIN".
	Elle crée l'application et gère les interactions entre l'utilisateur et l'application.

Lors de son initialisation, la fenêtre est créée, ainsi que le menu, l'automate et les
différentes options. Les valeurs de départ sont initialisées, puis les boutons sont connectés
à leur(s) fonction(s).

Dans cette classe, on trouve :
- une fonction d'actualisation pour chaque option
- une fonction de connexion qui associe, à chaque bouton, une ou plusieurs fonction(s)
- une fonction d'initialisation des variables globales
- une ou plusieurs fonction(s) exécutant l'action de chaque bouton
- plusieurs fonctions de traduction (entre seuil et densité, entre syntaxe interne et syntaxe externe des règles)
- une fonction d'actualisation de l'automate
"""

class Main :

	# Fonction d'initialisation
	def __init__(self) :
		self.application = QApplication(sys.argv) # Création de l'application
		mainWindow = QMainWindow() # Création de la fenêtre principale
		mainWindow.setWindowTitle(qApp.trUtf8("CellMates")) # Titre de la fenêtre principale
		mainWindow.resize(800, 600)
		window = QWidget() # Création du widget central
		mainLayout = QHBoxLayout() # Création du layout principal

		menu = QMenuBar(mainWindow) # Création du menu
		fichier = QMenu(qApp.trUtf8("&Fichier")) # Premier menu
		self.new = fichier.addAction(qApp.trUtf8("&Nouveau"))
		self.save = fichier.addAction(qApp.trUtf8("&Sauvegarder"))
		self.load = fichier.addAction(qApp.trUtf8("&Charger"))
		self.quit = fichier.addAction(qApp.trUtf8("&Quitter"))
		sauvegardes = QMenu(qApp.trUtf8("&Sauvegardes")) # Second menu
		self.never = sauvegardes.addAction(qApp.trUtf8("&Jamais"))
		self.always = sauvegardes.addAction(qApp.trUtf8("&Toujours"))
		self.customSaves = sauvegardes.addAction(qApp.trUtf8("&Personnalisé"))
		menu.addMenu(fichier) # Attachement du premier menu
		menu.addMenu(sauvegardes) # Attachement du second menu
		mainWindow.setMenuBar(menu) # Attachement de la barre de menu à la fenêtre principale

		# Création de la grille
		self.grille = Widgets.Grille()
		grilleView = Widgets.GrilleView(self.grille)
		mainLayout.addWidget(grilleView)

		self.initGlobals() # Initialisation des variables globales

		self.autoCell = autoCellmodif.AutoCell(Globals.grilleLignes, Globals.grilleColonnes,
				      					    Globals.grille, Globals.nombreEtats,
				      					    Globals.torus, Globals.tailleVoisinage,
				    					    Globals.regles, Globals.poidsVoisinage,
				   						    self.densiteToSeuil(Globals.densite)) # Création de l'automate

		# Création des options
		optionsArea = QScrollArea()
		optionsArea.setFixedWidth(630)
		self.options = Options.Options()
		self.resetOptions() # Initialisation des options
		optionsArea.setWidget(self.options)
		mainLayout.addWidget(optionsArea)

		self.timer = QTimer() # Création du timer permettant le défilement automatique
		self.timer.setInterval(5000/Globals.vitesse) # Initialisation de la vitesse de défilement

		self.connexions() # Connexion des boutons

		window.setLayout(mainLayout) # Attachement du layout principal au widget central
		mainWindow.setCentralWidget(window) # Attachement du widget central à la fenêtre principale

		mainWindow.show() # Affichage de l'application
		self.application.exec_() # Boucle principale


	# Fonction d'initialisation des variables globales
	def initGlobals(self) :
		Globals.nombreEtats = 2
		Globals.colors = [QColor("white"), QColor("black"),  QColor("red"), QColor(0,255,0), QColor("blue")]
		Globals.selectedState = 0
		Globals.densite = 2 * [50] + 3 * [0]
		Globals.tailleVoisinage = 5 * [1]
		Globals.typeVoisinage = 5 * [1]
		Globals.totalistique = 5 * [True]
		Globals.poidsVoisinage = []
		for i in range(5) :
			Globals.poidsVoisinage.append([])
			taille = Globals.tailleVoisinage[i]
			diametre = (taille + 1) * 2 - 1
			for j in range(diametre) :
				Globals.poidsVoisinage[i].append([])
				for k in range(diametre) :
					if j == taille and k == taille :
						Globals.poidsVoisinage[i][j].append(0)
					else :
						Globals.poidsVoisinage[i][j].append(1)
		Globals.torus = False
		Globals.grilleLignes = 20
		Globals.grilleColonnes = 20
		Globals.regles = {0 : {1 : {1 : [3]}}, 1 : {1 : {1 : [2, 3]}}}
		Globals.vitesse = 5
		Globals.etape = 0
		self.grille.initGrille()


	# Fonction mettant à jour le nombre d'états sélectionnés par l'utilisateur
	def resetNombreEtats(self) :
		for i in range(4) :
			if i == Globals.nombreEtats - 2 :
				self.options.etats[i].setChecked(True)
			else :
				self.options.etats[i].setChecked(False)

		for i in range(Globals.nombreEtats) :
			self.options.couleur[i].setEnabled(True)
		for i in range(Globals.nombreEtats, 5) :
			self.options.couleur[i].setEnabled(False)


	# Fonction mettant à jour les couleurs correspondant aux états
	def resetCouleurs(self) :
		for i in range(5) :
			self.options.couleur[i].setColor(Globals.colors[i])


	# Fonction mettant à jour l'état sélectionné
	def resetSelectedColor(self) :
		for i in range(5) :
			if i == Globals.selectedState :
				self.options.couleur[i].setSize(QSize(30, 30))
			else :
				self.options.couleur[i].setSize(QSize(20, 20))


	# Fonction ré-initialisant les densités de couleur (à un pourcentage équitable)
	def resetSeuils(self) :
		seuil = 100 // Globals.nombreEtats
		gap = 100 % Globals.nombreEtats

		for i in range(5) :
			self.options.densites[i].setValue(0)

		self.options.densites[0].setValue(seuil+gap)

		for i in range(1, Globals.nombreEtats) :
			self.options.densites[i].setValue(seuil)

		self.setMaxDensite()


	# Fonction mettant à jour la taille du voisinage
	def resetTailleVoisinage(self) :
		for i in range(3) :
			if i == Globals.tailleVoisinage[Globals.selectedState] - 1 :
				self.options.taille[i].setChecked(True)
			else :
				self.options.taille[i].setChecked(False)


	# Fonction mettant à jour le voisinage
	def resetVoisinage(self) :
		if Globals.typeVoisinage[Globals.selectedState] == 1 :
			self.options.moore.setChecked(True)
			self.options.vonNeumann.setChecked(False)
			self.options.voisinagePerso.setChecked(False)
		elif Globals.typeVoisinage[Globals.selectedState] == 2 :
			self.options.moore.setChecked(False)
			self.options.vonNeumann.setChecked(True)
			self.options.voisinagePerso.setChecked(False)
		else :
			self.options.moore.setChecked(False)
			self.options.vonNeumann.setChecked(False)
			self.options.voisinagePerso.setChecked(True)

		self.options.nonTotalistique.setChecked(not Globals.totalistique[Globals.selectedState])

		if Globals.typeVoisinage[Globals.selectedState] :
			self.options.aleatoire.setEnabled(False)
			self.options.nonTotalistique.setEnabled(False)
			poids = self.autoCell._genPoids(Globals.typeVoisinage[Globals.selectedState], True, Globals.tailleVoisinage[Globals.selectedState])
			Globals.poidsVoisinage[Globals.selectedState] = poids
		else :
			self.options.aleatoire.setEnabled(True)
			self.options.nonTotalistique.setEnabled(True)

		self.options.voisinageConfig.update()


	# Fonction mettant à jour le type de grille
	def resetTypeGrille(self) :
		self.options.toroidale.setChecked(Globals.torus)
		self.options.bornee.setChecked(not Globals.torus)


	# Fonction mettant à jour la taille de la grille
	def resetTailleGrille(self) :
		self.options.ligne.setValue(Globals.grilleLignes)
		self.options.colonne.setValue(Globals.grilleColonnes)


	# Fonction mettant à jour l'affichage des règles
	def resetRegles(self) :
		self.options.listeRegles.setText(qApp.trUtf8(self.reglesIntToExt(Globals.regles)))


	# Fonction mettant à jour la vitesse de défilement
	def resetVitesse(self) :
		self.options.vitesseSlider.setValue(Globals.vitesse)


	# Fonction ré-initialisant l'étape courante
	def resetEtape(self) :
		Globals.etape = 0
		self.options.numEtape.setValue(0)


	# Fonction remettant toutes les options à jour
	def resetOptions(self) :
		self.resetNombreEtats()
		self.resetCouleurs()
		self.resetSelectedColor()
		self.resetSeuils()
		self.resetTypeGrille()
		self.resetTailleGrille()
		self.resetTailleVoisinage()
		self.resetVoisinage()
		self.resetRegles()
		self.resetVitesse()
		self.resetEtape()


	# Fonction ré-initialisant toutes les options
	def resetAll(self) :
		self.initGlobals()
		self.resetOptions()


	# def saveAutomate(self) :
		# A ajouter avec les sauvegardes


	# def loadAutomate(self) :
		# A ajouter avec les sauvegardes


	# Fonction connectant les boutons à leur(s) fonction(s)
	def connexions(self) :
		self.new.triggered.connect(lambda: self.resetAll())
		self.quit.triggered.connect(lambda: self.application.quit())
		# self.save.triggered.connect(lambda: self.saveAutomate())
		# self.load.triggered.connect(lambda: self.loadAutomate())

		self.options.etats[0].pressed.connect(lambda: self.numberEtats(2))
		self.options.etats[1].pressed.connect(lambda: self.numberEtats(3))
		self.options.etats[2].pressed.connect(lambda: self.numberEtats(4))
		self.options.etats[3].pressed.connect(lambda: self.numberEtats(5))

		self.options.couleur[0].doubleClicked.connect(lambda: self.colorPick(0))
		self.options.couleur[0].clicked.connect(lambda: self.selectColor(0))
		self.options.couleur[1].doubleClicked.connect(lambda: self.colorPick(1))
		self.options.couleur[1].clicked.connect(lambda: self.selectColor(1))
		self.options.couleur[2].doubleClicked.connect(lambda: self.colorPick(2))
		self.options.couleur[2].clicked.connect(lambda: self.selectColor(2))
		self.options.couleur[3].doubleClicked.connect(lambda: self.colorPick(3))
		self.options.couleur[3].clicked.connect(lambda: self.selectColor(3))
		self.options.couleur[4].doubleClicked.connect(lambda: self.colorPick(4))
		self.options.couleur[4].clicked.connect(lambda: self.selectColor(4))

		self.options.densites[0].valueChanged.connect(lambda: self.setDensite(self.options.densites[0].value(), 0))
		self.options.densites[1].valueChanged.connect(lambda: self.setDensite(self.options.densites[1].value(), 1))
		self.options.densites[2].valueChanged.connect(lambda: self.setDensite(self.options.densites[2].value(), 2))
		self.options.densites[3].valueChanged.connect(lambda: self.setDensite(self.options.densites[3].value(), 3))
		self.options.densites[4].valueChanged.connect(lambda: self.setDensite(self.options.densites[4].value(), 4))
		self.options.ranDensite.clicked.connect(lambda: self.genRandomDensite())

		self.options.taille[0].pressed.connect(lambda: self.setTailleVois(1))
		self.options.taille[1].pressed.connect(lambda: self.setTailleVois(2))
		self.options.taille[2].pressed.connect(lambda: self.setTailleVois(3))

		self.options.moore.pressed.connect(lambda: self.setTypeVoisinage(1))
		self.options.vonNeumann.pressed.connect(lambda: self.setTypeVoisinage(2))
		self.options.voisinagePerso.pressed.connect(lambda: self.setTypeVoisinage(0))
		self.options.aleatoire.clicked.connect(lambda: self.genRandomVois())
		self.options.nonTotalistique.stateChanged.connect(lambda: self.setTotalistique())

		self.options.bornee.pressed.connect(lambda: self.setType(False))
		self.options.toroidale.pressed.connect(lambda: self.setType(True))

		self.options.ligne.valueChanged.connect(lambda: self.setLigne())
		self.options.colonne.valueChanged.connect(lambda: self.setColonne())

		self.options.mono.clicked.connect(lambda: self.genMonocolore())
		self.options.random.clicked.connect(lambda: self.genRandomGrille())

		self.options.randomRegles.clicked.connect(lambda: self.genRandomRegles())
		self.options.persoRegles.clicked.connect(lambda: self.openChoixRegles())

		self.options.vitesseSlider.valueChanged.connect(lambda: self.setVitesse())

		self.options.numEtape.valueChanged.connect(lambda: self.etapeN())
		# self.options.precedent.clicked.connect(lambda: self.etapePrecedente())
		self.options.play.clicked.connect(lambda: self.play())
		self.options.pause.clicked.connect(lambda: self.pause())
		self.options.suivant.clicked.connect(lambda: self.etapeGrille())

		self.options.gen.clicked.connect(lambda: self.genRandom())

		self.timer.timeout.connect(lambda: self.etapeGrille())


	# Fonction de sélection du nombre d'états
	def numberEtats(self, nb) :
		Globals.nombreEtats = nb
		Globals.selectedState = 0
		Globals.regles = False
		self.resetNombreEtats()
		self.resetCouleurs()
		self.resetSelectedColor()
		self.resetSeuils()
		self.resetRegles()
		self.genMonocolore()
		self.autoCellUpdate()
		self.setMaxDensite()


	# Fonction de sélection d'un état/couleur
	def selectColor(self, etat) :
		Globals.selectedState = etat
		self.resetSelectedColor()
		self.resetTailleVoisinage()
		self.resetVoisinage()


	# Fonction de personnalisation de couleur
	def colorPick(self, etat) :
		if self.options.couleur[etat].isEnabled() :
			color = QColorDialog.getColor(self.options.couleur[etat].getColor())
			if color.isValid() :
				Globals.colors[Globals.selectedState] = color
				self.resetCouleurs()
				self.grille.update()


	# Fonction de sélection des densités de couleur sur la grille
	def setDensite(self, densite, etat) :
		Globals.densite[etat] = densite
		self.setMaxDensite()


	# Fonction mettant à jour les maximums de densités dynamiquement (afin de toujours avoir un total de 100% maximum)
	def setMaxDensite(self) :
		nb = Globals.nombreEtats
		d1 = self.options.densites[0].value()
		d2 = self.options.densites[1].value()
		d3 = self.options.densites[2].value()
		d4 = self.options.densites[3].value()
		d5 = self.options.densites[4].value()

		self.options.densites[0].setMaximum(100 - d2 - d3 - d4 - d5)
		self.options.densites[1].setMaximum(100 - d1 - d3 - d4 - d5)

		if nb >= 3 :
			self.options.densites[2].setMaximum(100 - d1 - d2 - d4 - d5)
		else :
			self.options.densites[2].setMaximum(0)
		if nb >= 4 :
			self.options.densites[3].setMaximum(100 - d1 - d2 - d3 - d5)
		else :
			self.options.densites[3].setMaximum(0)
		if nb == 5 :
			self.options.densites[4].setMaximum(100 - d1 - d2 - d3 - d4)
		else :
			self.options.densites[4].setMaximum(0)


	# Fonction générant des densités aléatoires
	def genRandomDensite(self) :
		self.autoCellUpdate()
		nb = Globals.nombreEtats
		seuils = self.autoCell._genSeuils(False)
		densites = self.seuilToDensite(seuils)

		for i in range(5) :
			self.options.densites[i].setValue(0)
		for i in range(5) :
			self.options.densites[i].setValue(densites[i])


	# Fonction de sélection de la taille du voisinage (pour l'état courant)
	def setTailleVois(self, taille) :
		etat = Globals.selectedState
		poids = self.autoCell._genPoids(1, True, taille)

		Globals.totalistique[etat] = True
		Globals.poidsVoisinage[etat] = poids
		Globals.typeVoisinage[etat] = 1
		Globals.tailleVoisinage[etat] = taille
		self.resetTailleVoisinage()
		self.resetVoisinage()


	# Fonction de sélection du type de voisinage (pour l'état courant)
	def setTypeVoisinage(self, typeVois) :
		Globals.typeVoisinage[Globals.selectedState] = typeVois
		self.resetVoisinage()


	# Fonction générant un voisinage aléatoire (pour l'état courant)
	def genRandomVois(self) :
		etat = Globals.selectedState
		poids = self.autoCell._genPoids(0, Globals.totalistique[etat], Globals.tailleVoisinage[etat])
		Globals.poidsVoisinage[etat] = poids
		self.resetVoisinage()


	# Fonction permettant de choisir un voisinage non totalistique (pour l'état courant)
	def setTotalistique(self) :
		totalistique = not self.options.nonTotalistique.isChecked()
		Globals.totalistique[Globals.selectedState] = totalistique
		self.resetVoisinage()


	# Fonction de sélection du type de grille
	def setType(self, typeGrille) :
		Globals.torus = typeGrille


	# Fonction de sélection de la hauteur de la grille
	def setLigne(self) :
		self.pause()
		Globals.grilleLignes = self.options.ligne.value()
		self.genMonocolore()


	# Fonction de sélection de la largeur de la grille
	def setColonne(self) :
		self.pause()
		Globals.grilleColonnes = self.options.colonne.value()
		self.genMonocolore()


	# Fonction générant une grille monocolore (pour la couleur sélectionnée)
	def genMonocolore(self) :
		self.resetEtape()
		self.grille.initGrille()


	# Fonction générant une grille aléatoire
	def genRandomGrille(self) :
		self.resetEtape()
		self.autoCellUpdate()
		Globals.grille = self.autoCell._genGrille()
		self.grille.update()


	# Fonction générant des règles aléatoires
	def genRandomRegles(self) :
		self.autoCellUpdate()
		regles = self.autoCell._genRegles()
		Globals.regles = regles
		self.resetRegles()


	# Fonction permettant de créer ses propres règles
	def openChoixRegles(self) :
		popup = Widgets.ReglesPopup()
		popup.setWindowTitle(qApp.trUtf8("Règles"))
		res = popup.exec_()
		if res :
			regles = self.reglesExtToInt(popup.texteRegles.toPlainText())
			Globals.regles = regles
			self.resetRegles()


	# Fonction de sélection de la vitesse de défilement
	def setVitesse(self) :
		Globals.vitesse = self.options.vitesseSlider.value()
		self.timer.setInterval(5000/Globals.vitesse)


	# Fonction permettant le défilement automatique
	def play(self) :
		self.timer.start()


	# Fonction mettant en pause le défilement automatique
	def pause(self) :
		self.timer.stop()


	# Fonction permettant de faire une étape
	def etapeGrille(self) :
		self.autoCellUpdate()
		self.autoCell.etape()
		Globals.grille = self.autoCell.grille
		self.grille.update()
		Globals.etape += 1
		self.options.numEtape.setValue(Globals.etape)


	# def etapePrecedente(self) :
		# A ajouter avec les sauvegardes


	# Fonction permettant de passer à l'étape n
	def etapeN(self) :
		diff = self.options.numEtape.value() - Globals.etape

		if diff > 0 :
			self.autoCellUpdate()
			for i in range(diff) :
				self.autoCell.etape()
			Globals.grille = self.autoCell.grille
			self.grille.update()
			Globals.etape = self.options.numEtape.value()
		# elif diff < 0 :
			# A ajouter avec les sauvegardes


	# Fonction permettant une génération aléatoire globale de l'automate
	def genRandom(self) :
		self.autoCell.__init__(Globals.grilleLignes, Globals.grilleColonnes,
				       False, Globals.nombreEtats,
				       Globals.torus, Globals.tailleVoisinage, False, False,
				       False, False, Globals.typeVoisinage, Globals.totalistique)
		Globals.grille = self.autoCell.grille
		Globals.regles = self.autoCell.regles
		Globals.poidsVoisinage = self.autoCell.poids
		densites = self.seuilToDensite(self.autoCell.seuils)

		for i in range(5) :
			self.options.densites[i].setValue(0)
		for i in range(5) :
			self.options.densites[i].setValue(densites[i])

		self.resetEtape()
		self.resetRegles()
		self.resetVoisinage()
		self.grille.update()


	# Fonction de conversion de densité vers seuil
	def densiteToSeuil(self, densites) :
		nb = Globals.nombreEtats
		seuils = [densites[0]]
		
		for i in range(1, nb) :
			seuils.append(densites[i] + seuils[i-1])

		gap = 100 - seuils[nb-1]
		if gap :
			self.options.densites[0].setValue(densites[0] + gap)
			Globals.densite[0] += gap
			for i in range(nb) :
				seuils[i] += gap

		return seuils


	# Fonction de conversion de seuil vers densité
	def seuilToDensite(self, seuils) :
		nb = Globals.nombreEtats
		densites = [seuils[0]]

		for i in range(1, nb) :
			densites.append(seuils[i] - seuils[i-1])

		for i in range(nb, 5) :
			densites.append(0)

		return densites


	# Fonction de conversion de la syntaxe interne des règles vers la syntaxe utilisateur
	def reglesIntToExt(self, regles) :
	# A modifier en fonction de la syntaxe utilisateur choisie.
	# Pour l'instant, les règles externes sont les mêmes que les règles internes
		string = (str(regles) if regles else "")
		return string


	# Fonction de conversion de la syntaxe utilisateur vers la syntaxe interne des règles
	def reglesExtToInt(self, regles) :
		try :
		# A modifier en fonction la syntaxe utilisateur choisie
			exec "r = " + regles
			rules = r
		except :
			# Du fait d'un bug avéré dans QMessageBox, afin de régler la taille de la box, il faut contourner le problème de cette façon :
			error = QMessageBox()
			error.setInformativeText(qApp.trUtf8("Syntaxe incorrecte"))
			error.setWindowTitle(qApp.trUtf8("Erreur"))
			spacer = QSpacerItem(300, 0, QSizePolicy.Minimum, QSizePolicy.Expanding)
			layout = error.layout()
			layout.addItem(spacer, layout.rowCount(), 0, 1, layout.columnCount())
			error.exec_()
			rules = Globals.regles

		return rules


	# Fonction mettant à jour l'automate
	def autoCellUpdate(self) :
		self.autoCell.__init__(Globals.grilleLignes, Globals.grilleColonnes,
				       Globals.grille, Globals.nombreEtats,
				       Globals.torus, Globals.tailleVoisinage,
				       Globals.regles, Globals.poidsVoisinage,
				       self.densiteToSeuil(Globals.densite))
		if not Globals.regles :
			Globals.regles = self.autoCell.regles
			self.resetRegles()


Main()
